package com.danguo.happymonkey;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;

import com.danguo.happymonkey.R;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
import android.opengl.GLUtils;
import android.opengl.Matrix;
import android.util.Log;

public class StarSystem
{
   private static final int BYTES_PER_FLOAT = 4;
   private static final int POS_DATA_SIZE = 3;
   private static final int POS_OFFSET = 0;
   
   /** Store our model data in a float buffer. */
   private final FloatBuffer thePoint;
   private FloatBuffer theTag;
   private float[] theCoords = new float[4];
   private Points.Star theName;
   private float[] theModelMatrix = new float[16];
   
   private int theProgramHandle;
   private int thePositionHandle;
   private int[] theTextures;
   private int theBaseMap;
   private int theMVPMatrixHandle;
   private int theProgramHandleLabel;
   private int theMVPMatrixHandleLabel;
   private int thePositionHandleLabel;
   private int theBaseMapLabel;

   /**
    * Takes in a world space coordinate in homogeneous form
    * @param aHomogeneousCoordinate the world-space location of the point
    */
   public StarSystem( GLSurfaceView aContext, float[] aHomogeneousCoordinate, 
                      Points.Star aName )
   {
      theCoords = aHomogeneousCoordinate;
      thePoint = ByteBuffer.allocateDirect(theCoords.length * BYTES_PER_FLOAT)
                           .order(ByteOrder.nativeOrder()).asFloatBuffer();

      thePoint.put(theCoords).position(0);
      Matrix.setIdentityM( theModelMatrix, 0 );
      
      theTag = ByteBuffer.allocateDirect(theCoords.length * BYTES_PER_FLOAT)
            .order(ByteOrder.nativeOrder()).asFloatBuffer();

      theTag.put(theCoords).position(0);
      
      
      
      theProgramHandle = createProgram(vertexShader, fragmentShader);
      theProgramHandleLabel = createProgram(vertexShaderLabel, fragmentShader);
      
      // Set program handles. These will later be used to pass in values to the
      // program.
      theMVPMatrixHandle = GLES20.glGetUniformLocation( theProgramHandle,
            "u_MVPMatrix" );
      
      theMVPMatrixHandleLabel = GLES20.glGetUniformLocation( theProgramHandleLabel,
            "u_MVPMatrix" );
      
      thePositionHandle = GLES20.glGetAttribLocation( theProgramHandle, "a_Position" );
      checkGlError("glGetAttribLocation aPosition");
      if (thePositionHandle == -1) {
         throw new RuntimeException("Could not get attrib location for aPosition");
      }      
      
      thePositionHandleLabel = GLES20.glGetAttribLocation( theProgramHandleLabel, "a_Position" );
      checkGlError("glGetAttribLocation aPosition Label");
      if (thePositionHandle == -1) {
         throw new RuntimeException("Could not get attrib location for aPosition");
      }      
      
      theBaseMap = GLES20.glGetUniformLocation(theProgramHandle, "u_baseMap");
      checkGlError("glGetUniformLocation uBaseMap");
      
      theBaseMapLabel = GLES20.glGetUniformLocation(theProgramHandleLabel, "u_baseMap");
      checkGlError("glGetUniformLocation uBaseMapLabel");
      
      /*
       * Create our texture. This has to be done each time the
       * surface is created.
       */
      theName = aName;
      Log.d("Utils", "Loadtexture");
      theTextures = new int[2];
      GLES20.glGenTextures(2, theTextures, 0);
      Bitmap img = BitmapFactory.decodeResource(aContext.getResources(), R.drawable.roundel);
      assignTexture( img, theTextures, 0 );
      
      switch( theName )
      {
      case ALTAIR:
         img = BitmapFactory.decodeResource(aContext.getResources(), R.drawable.tag_altair);
         assignTexture( img, theTextures, 1);
         break;
      case DENEB:
         img = BitmapFactory.decodeResource(aContext.getResources(), R.drawable.tag_deneb);
         assignTexture( img, theTextures, 1);
         break;
      case ORION:
         img = BitmapFactory.decodeResource(aContext.getResources(), R.drawable.tag_orion);
         assignTexture( img, theTextures, 1);
         break;
      default:
         break;
      }      
      img.recycle();
   }
   
   private void assignTexture( Bitmap anImg, int[] aTextureArray, int anIndex )
   {
      GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, aTextureArray[anIndex]);
      GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
      GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
      GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S,
            GLES20.GL_CLAMP_TO_EDGE);
      GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T,
            GLES20.GL_CLAMP_TO_EDGE);
      GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, anImg, 0);
      Log.d("LoadTexture", "Loaded texture"+":H:"+anImg.getHeight()+":W:"+anImg.getWidth());
      checkGlError( "Load Texture " + anIndex );
   }
   
   /**
    * Draws this point as a point sprite
    */
   public void draw( float[] aVPMatrix )
   {
	  /*
	   * Draw the marker that signifies a star system as a textured point.
	   */
      GLES20.glUseProgram( theProgramHandle );
      checkGlError("glUseProgram");
      thePoint.position(0);
      GLES20.glVertexAttribPointer(thePositionHandle, POS_DATA_SIZE, GLES20.GL_FLOAT, 
                                   false, POS_OFFSET, this.thePoint);
      
      GLES20.glEnableVertexAttribArray(thePositionHandle);
     
      GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
      GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, theTextures[0]);
      checkGlError("glBindTexture theTextures[0]");
     
      GLES20.glUniform1i(theBaseMap, 0);


      GLES20.glUniformMatrix4fv( theMVPMatrixHandle, 1, false, 
                                 aVPMatrix, 0 );

      GLES20.glDrawArrays(GLES20.GL_POINTS, 0, 1);  
      GLES20.glDisableVertexAttribArray(thePositionHandle);
      
      /**
	   * Draw the label for the star system as a textured point.
	   */
      GLES20.glValidateProgram( theProgramHandleLabel );
      GLES20.glUseProgram( theProgramHandleLabel );
      checkGlError("glUseProgramLabel");
      theTag.position(0);
      GLES20.glVertexAttribPointer(thePositionHandleLabel, POS_DATA_SIZE, GLES20.GL_FLOAT, 
                                   false, POS_OFFSET, this.theTag);
      
      GLES20.glEnableVertexAttribArray(thePositionHandleLabel);
     
      GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
      GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, theTextures[1]);
      checkGlError("glBindTexture theTextures[1]");
     
      GLES20.glUniform1i(theBaseMapLabel, 0);
      GLES20.glUniformMatrix4fv( theMVPMatrixHandleLabel, 1, false, aVPMatrix, 0 );
      
      GLES20.glDrawArrays(GLES20.GL_POINTS, 0, 1);   
      checkGlError("glDrawArrays");
      
      GLES20.glDisableVertexAttribArray(thePositionHandleLabel);

   }
   
   /**
    * Calculate the screen coordinates of this point
    * @param aScreenHeight the height of the context view
    * @param aScreenWidth the width of the context view
    * @return the screen coordinates. [0] = x , [1] = y
    */
   public int[] getScreenCoords( float[] aVPMatrix, 
                                 int aScreenHeight, int aScreenWidth )
   {
      float[] resultV = new float[4];
      
      Matrix.multiplyMV( resultV, 0, aVPMatrix, 0, theCoords, 0 ); 
      
      //divide resultV[0]/resultV[3] because they represent homogeneous
      //coordinate values for x and w. x/w normalizes the coordinate
      int winX = (int) Math.round( ( ( resultV[0]/resultV[3] + 1 ) / 2.0) *
            aScreenWidth );
      //we calculate -point3D.getY() because the screen Y axis is
      //oriented top->down 
      int winY = (int) Math.round((( 1 - resultV[1]/resultV[3] ) / 2.0) *
            aScreenHeight ) ;
      int[] result = new int[2];
      result[0] = winX;
      result[1] = winY;
      return result;
   }
   public float[] getTheCoords()
   {
      return theCoords;
   }

   public void setTheCoords( float[] theTheCoords )
   {
      theCoords = theTheCoords;
   }
   
   final String vertexShader = 
         "uniform mat4 u_MVPMatrix; " + // A constant
         "attribute vec4 a_Position;" + // Per-vertex position info we will pass in.
        // "attribute vec2 aTextureCoord;" +
        // "varying vec2 vTextureCoord;" +
         "void main()" + 
         "{" + 
            "gl_PointSize = 45.0;" + 
            "gl_Position = u_MVPMatrix*a_Position;" +
         //   "vTextureCoord = aTextureCoord;" +
         "}";
   
   final String vertexShaderLabel = 
         "uniform mat4 u_MVPMatrix; " + // A constant
         "attribute vec4 a_Position;" + // Per-vertex position info we will pass in.
         "vec4 temp_pos;" + 
         "vec4 zCorrection = vec4(0.2, 0, 0, 0);" +
         "vec4 xyOffset = vec4(0.3, -0.2, 0, 0);" +
        // "attribute vec2 aTextureCoord;" +
        // "varying vec2 vTextureCoord;" +
         "void main()" + 
         "{" + 
            "gl_PointSize = 60.0;" + 
            "temp_pos = u_MVPMatrix*a_Position;" +
            "gl_Position = temp_pos + zCorrection*temp_pos.z + xyOffset;" +
         //   "vTextureCoord = aTextureCoord;" +
         "}";

   final String fragmentShader = 
         "precision mediump float;" + 
        // "varying vec2 vTextureCoord;" +
        // "uniform sampler2D sTexture;" +
         "uniform sampler2D u_baseMap;" +
         "void main()" +
         "{" +
            "gl_FragColor = texture2D(u_baseMap, gl_PointCoord);" +
            //"gl_FragColor = texture2D(sTexture, vTextureCoord);" +
         "}";
   
/*******************************UTILITY METHODS**********************************
   
   /**
    * Loads and compiles a shader
    * @param shaderType integer from GLES20 representing the type of shader
    * @param source the C code describing the shader
    * @return the handle to the compiled shader
    */
   private int loadShader(int shaderType, String source) {
      int shader = GLES20.glCreateShader(shaderType);
      if (shader != 0) {
          GLES20.glShaderSource(shader, source);
          GLES20.glCompileShader(shader);
          int[] compiled = new int[1];
          GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0);
          if (compiled[0] == 0) {
              Log.e("GLES20Points", "Could not compile shader " + shaderType + ":");
              Log.e("GLES20Points", GLES20.glGetShaderInfoLog(shader));
              GLES20.glDeleteShader(shader);
              shader = 0;
          }
      }
      return shader;
  }
   
   /**
    * Combines vertex and fragment shaders into a compiled program
    * @param vertexSource the C code describing the vertex shader
    * @param fragmentSource the C code describing the fragment shader
    * @return a handle to the compiled program
    */
   private int createProgram(String vertexSource, String fragmentSource) {
         int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource);
         if (vertexShader == 0) {
             return 0;
         }

         int pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource);
         if (pixelShader == 0) {
             return 0;
         }

         int program = GLES20.glCreateProgram();
         if (program != 0) {
             GLES20.glAttachShader(program, vertexShader);
             checkGlError("glAttachVShader");
             GLES20.glAttachShader(program, pixelShader);
             checkGlError("glAttachFShader");
             GLES20.glLinkProgram(program);
             int[] linkStatus = new int[1];
             GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0);
             if (linkStatus[0] != GLES20.GL_TRUE) {
                 Log.e("GLES20Points", "Could not link program: ");
                 Log.e("GLES20Points", GLES20.glGetProgramInfoLog(program));
                 GLES20.glDeleteProgram(program);
                 program = 0;
             }
         }
         return program;
     }
   
   /**
    * Checks GL for errors
    * @param op String to send to the log if an error is found.
    */
   private void checkGlError(String op) {
      int error;
      while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) {
          Log.e("GLES20Points", op + ": glError " + error);
          throw new RuntimeException(op + ": glError " + error);
      }
  }
}
